This is the quick walkthrough of implementing and executing a HTTP server that just return hello world by using Erlang, Cowboy and rebar3.
The goal is, while executing this problem, the following happens.
$ curl "htttp://localhost:8080/"
hello,world
Erlang and Cowboy is easier compared to the god-forsaken rebar3. Rebar3, as its name indicates, had major backward incompatible breaking changes twice, and it still has many technical debt piled up on it. Even the official documentation is unhelpful at this time. I wasated months just figuring out the basics and I write it down here so you don't have to.
Prerequisites
You need Erlang implementation, rebar3. The cowboy can be fetched by rebar3.
To install erlang on debian-based distro:
$ apt install erlang
To install rebar3 in most GNU/Linux distros, you should built it yourself.
$ git clone https://github.com/erlang/rebar3.git
$ cd rebar3
$ ./bootstrap
This create a file "rebar3". You can copy this file to somewhere in the PATH or symlink it.
ln -s rebar3 /usr/local/bin
Now you're ready to start using the rebar3.
Creating a new project.
First, you must create a new rebar3 project. Let's call it "hello_server".
$ rebar3 new release hello_server
$ cd hello_server
This command create a directory "hello_server" and bunch of template files in that directory. I don't explain everything in details, but just for the important bits.
"hello_server/rebar.config" is a config file we have to modify to add cowboy as an dependency.
"hello_server/apps" directory contains apps. rebar3's release project is called "umbrella project", it can contains multiple "apps".
"hello_server/apps/hello_server" is the default apps the rebar3 generated from the template. Inside that directory, there is a "src" directory which contains three template files. "hello_server_app.erl", "hello_server_sup.erl" and "hello_server.app.src".
"hello_server_app.erl" is a source file we modify.
"hello_server_sup.erl" is for implementing supervisor behaviour. We don't modify this file in this walkthrough.
"hello_server.app.src" is a source file for application resource file. It tell the Erlang VM how to start the application. Rebar3 just copy it to the appropriate place so you don't have to. We modify this file too.
Adding cowboy as an dependency
Next, we need to add cowboy as an dependency so the rebar3 can fetch it. To do so, open the "hello_server/rebar.config".
$ vim rebar.config
The first few lines are like these.
[erl_opts, [debug_info]}.
{deps, []}.
[relx, [{release, {hello_server, "0.1.0"},
...
We need to modify the second line, that is "{deps, []}.". You can add dependencies in the list. There are many formats for that but every thing is tuple of "{ package_name, ... }". In this walkthrough, we fetch the package from hex.pm. So the format shall be "{ package_name, "version number"}". As of this writing, the latest stable version of cowboy is 2.7.0.
{deps, [
{cowboy, "2.7.0"}
]}.
rebar3 fetch the necessary dependency automatically when it is needed, but let's just fetch it explicitly to make sure we wrote it correctly.
$ rebar3 upgrade
Also, we need to modify the application resource file to start cowboy before our application. Since our application requires cowboy, the cowboy application must be started before our application. To do so, modify the "hello_server.app.src"
$ vim apps/hello_server/src/hello_server.app.src
The part of the content of this file should looks like this.
{application, hello_server,
[...
{applications
[kernel,
stdlib
]},
...
]}.
We add "cowboy" to the list.
{application, hello_server,
[...
{applications
[kernel,
stdlib, % don't forget comma
cowboy % add this line
]},
...
]}.
As you see, this is Erlang's list so don't forget the comma before cowboy.
Fire up the HTTP server
Now we're going to start the HTTP server. First, we modify the "apps/hello_server/src/hello_server_app.erl".
vim apps/hello_server/src/hello_server_app.erl
This source code is generated by rebar3 to implement the application behaviour. We are going to modify the start/2 to fire up the HTTP server.
start(_StartType, _StartArgs) ->
hello_server_sup:start_link().
In order to start the HTTP server listening the incoming connection, we first need to set what cowboy call it "route". It's a mapping of the connection from some remote hosts, path to cowboy_handler. To do that, we use cowboy_router:compile/1 which take a parameter of type cowboy_router:routes(). The type is "[{Host, PathList}]", if you expand the PathList type, it'll be "[Host [{Path, Handler, InitialState}]]".
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{ Host, [{Path, Handler, InitialState}]
]),
hello_server_sup:start_link().
The Host can be '_' which means we allow the connections from any hosts. If you are to allow connection from anywhere, use '_', if on the other hand, you want to restrict the access only from, say, localhost, it would be <<"localhost">>.
The Path in our case is <<"/">>. Since we don't support path like "http://localhost/aa/bb/cc".
For Handler, we specify "hello_hander" atom which we have to implement it as a cowboy_handler behaviour later.
We don't use state so the IinitialState be emply list.
Putting all togather so far, the code looks like this.
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{ <<"localhost">>, [{<<"/">>, hello_handler, [] }]
]),
hello_server_sup:start_link().
Now we prepared the route, we're going to fire up the HTTP listener. We are going to use good old plaintext HTTP by cowboy:start_clear/3. The three parameters are "start_clear(Name, TransportOpts, ProtocolOpts)".
Name can be any Erlang term to refer this listener, but atom used used most of the time. Let's use "hello_listener".
TransportOpts has many options, but for this walkthrough, we only need to set the port to listen to. We going to listen port 8080 so it would be "[{port, 8080}]".
In ProtocolOpts, we use the route we made earler. The type of ProtocolOpts is a map which as a key env whose value is also a map which has a key dispatch. We pass Dispatch for the value of this key.
If succeeded, start_claer/2 return "{ok, pid}". Let's make sure it returns ok and fail otherwise.
start(_StartType, _StartArgs) ->
Dispatch = cowboy_router:compile([
{ <<"localhost">>, [{<<"/">>, hello_handler, []}] }
]),
{ok, _} = cowboy:start_clear(
ello_listener,
[{port, 8080}],
#{env => #{dispatch => Dispatch}}
),
hello_server_sup:start_link().
Handling the incoming connections.
Now the HTTP listners are up and running, we need to implement the hander for the incoming connections. For that, we need to implement the hello_handler we specified earlier by following the cowboy_hander behaviour. Create a new source file
.
$ vim apps/hello_server/src/hello_handler.erl
Let's write the basics.
-module(hello_handler).
-behaviour(cowboy_handler).
-export([init/2]).
init( Req, State ) ->
{ok, Req, State}.
Req represents requests and response. We don't use State really. It's just an empty list.
All we need to do is return a HTTP status code 200, the content-type is text/plain, it's content is "hello,world". We can do that by cowboy_req:reply/4. The parameters are "reply(Status, Headers, Body, Req)".
Status is a HTTP status code in non_neg_integer() or binary(). In this case, it's 200.
Header is a map() to specify HTTP header. we set it's "content-type" to be "text/plain".
For the Body, we return "hello,world".
Req is a current Req object.
reply return a new Req object which we must use it instead of old Req objects after the call.
And now, the init/2 code.
init( Req, State ) ->
Req_1 = cowboy_req:reply(
200,
#{<<"content-type">> => <<"text/plain">>},
<<"hello,world">>,
Req
),
{ok, Req, State}.
To run the program
$ rebar shell
Now to confirm it.
$ curl "http://localhost:8080/"
hello,world
If I have in the mood, I'll write the error handling next. The reality is more verbose than textbook.